/**
 * Copyright 2015 - 2016 KeepSafe Software, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.getkeepsafe.relinker;

import com.getkeepsafe.relinker.elf.Constants;
import com.getkeepsafe.relinker.elf.LogUtils;

import ohos.app.Context;

import java.io.*;

import java.util.Enumeration;
import java.util.HashSet;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

/**
 * Function description
 * class ApkLibraryInstaller
 */
public class ApkLibraryInstaller implements ReLinker.LibraryInstaller {
    private static final int MAX_TRIES = 5;

    private static final int COPY_BUFFER_SIZE = 4096;

    private Context context = null;

    private ReLinkerInstance hmosInstance = null;

    /**
     * Method sourceDirectories
     *
     * @param icontext Context
     * @return string
     */
    private String[] sourceDirectories(final Context icontext) {
        final ohos.bundle.ApplicationInfo appInfo = icontext.getApplicationInfo();
        String[] strArr = new String[appInfo.getModuleSourceDirs().size()];
        appInfo.getModuleSourceDirs().toArray(strArr);
        return strArr;
    }

    /**
     * Function description
     * class ZipFileInZipEntry
     */
    private static class ZipFileInZipEntry {
        private ZipFile zipFile;

        private ZipEntry zipEntry;

        /**
         * Method ZipFileInZipEntry
         *
         * @param zipFile ZipFile
         * @param zipEntry ZipEntry
         */
        protected ZipFileInZipEntry(ZipFile zipFile, ZipEntry zipEntry) {
            this.zipFile = zipFile;
            this.zipEntry = zipEntry;
        }
    }

    /**
     * Method sourceDirectories
     *
     * @param icontext Context
     * @param abis String[]
     * @param mappedLibraryName String
     * @param instance RelinkerInstance
     * @return null
     */
    private ZipFileInZipEntry findapkWithLibrary(final Context icontext, final String[] abis,
        final String mappedLibraryName, final ReLinkerInstance instance) {
        ZipFileInZipEntry zipFileInZipEntry = null;
        String[] srcDirs;
        srcDirs = sourceDirectories(context);

        for (String sourceDir : srcDirs) {
            ZipFile zipFile = null;
            int tries = 0;
            while (tries++ < MAX_TRIES) {
                try {
                    zipFile = new ZipFile(new File(sourceDir), ZipFile.OPEN_READ);
                    break;
                } catch (IOException ignored) {
                    LogUtils.info(Constants.BUNDLE_NAME, Constants.TAG_VAL);
                }
            }

            if (zipFile == null) {
                continue;
            }

            tries = 0;
            while (tries++ < MAX_TRIES) {
                String jniNameInApk = null;
                ZipEntry libraryEntry = null;

                for (final String abi : abis) {
                    jniNameInApk = Constants.LIBS + File.separatorChar + abi + File.separatorChar + mappedLibraryName;
                    hmosInstance.logDetails("Looking for %s in HAP %s...", jniNameInApk, sourceDir);

                    libraryEntry = zipFile.getEntry(jniNameInApk);

                    if (libraryEntry != null) {
                        zipFileInZipEntry = new ZipFileInZipEntry(zipFile, libraryEntry);
                    }
                }
            }

            try {
                zipFile.close();
            } catch (IOException ignored) {
                hmosInstance.logDetails("IOException : %s", ignored.getLocalizedMessage());
            }
        }

        return zipFileInZipEntry;
    }

    /**
     * Loop over all APK's again in order to detect which ABI's are actually supported.
     * This second loop is more expensive than trying to find a specific ABI, so it should
     * only be ran when no matching libraries are found. This should keep the overhead of
     * the happy path to a minimum.
     *
     * @param icontext Context
     * @param mappedLibraryName String
     * @return stringArray
     * @throws IOException
     */
    private String[] getSupportedAbis(Context icontext, String mappedLibraryName) throws IOException {
        String path = Constants.LIBS + File.separatorChar + Constants.ABIS_PATH_ONE + File.separatorChar
                + Constants.ABIS_PATH_TWO + File.separatorChar + mappedLibraryName;
        Pattern pattern = Pattern.compile(path);
        ZipFile zipFile;
        Set<String> supportedAbies = new HashSet<String>();

        String[] srcDirs = sourceDirectories(context);

        for (String sourceDir : srcDirs) {
            try {
                zipFile = new ZipFile(new File(sourceDir), ZipFile.OPEN_READ);
            } catch (IOException ignored) {
                continue;
            }

            Enumeration<? extends ZipEntry> elements = zipFile.entries();
            while (elements.hasMoreElements()) {
                ZipEntry el = elements.nextElement();
                Matcher match = pattern.matcher(el.getName());
                if (match.matches()) {
                    supportedAbies.add(match.group(1));
                }
            }
        }

        String[] result = new String[supportedAbies.size()];
        return supportedAbies.toArray(result);
    }

    /**
     * Attempts to unpack the given library to the given destination. Implements retry logic for
     * IO operations to ensure they succeed.
     *
     * @param icontext {@link Context} to describe the location of the installed APK file
     * @param abis abis
     * @param mappedLibraryName The mapped name of the library file to load
     * @param destination destination
     * @param logger logger
     */
    @Override
    public void installLibrary(Context icontext, String[] abis, String mappedLibraryName, File destination,
        ReLinkerInstance logger) {
        context = icontext;
        hmosInstance = logger;
        ZipFileInZipEntry found = null;
        try {
            found = findapkWithLibrary(null, abis, mappedLibraryName, null);
            if (found == null) {
                String[] supportedAbis = getSupportedAbis(null, mappedLibraryName);
                throw new MissingLibraryException(mappedLibraryName, abis, supportedAbis);
            }
            int tries = 0;
            while (tries++ < MAX_TRIES) {
                try {
                    if (!destination.exists() && !destination.createNewFile()) {
                        continue;
                    }
                } catch (IOException ignored) {
                    continue;
                }
                InputStream inputStream = null;
                FileOutputStream fileOut = null;
                try {
                    inputStream = found.zipFile.getInputStream(found.zipEntry);
                    fileOut = new FileOutputStream(destination);
                    final long written = copy(inputStream, fileOut);
                    fileOut.getFD().sync();
                    if (written != destination.length()) {
                        continue;
                    }
                } catch (IOException e) {
                    continue;
                } finally {
                    closeSilently(inputStream);
                    closeSilently(fileOut);
                }
                setValues(destination);
                return;
            }
        } catch (IOException e) {
            LogUtils.debug(Constants.BUNDLE_NAME, "installLibrary IOExcaption");
        } finally {
            finallyMethod(found);
        }
    }

    private void setValues(File destination) {
        if (destination.setReadable(true, false)) {
            LogUtils.info(Constants.BUNDLE_NAME, "setReadable is successful");
        }
        if (destination.setExecutable(true, false)) {
            LogUtils.info(Constants.BUNDLE_NAME, "setExecutable is successful");
        }
        if (destination.setWritable(true)) {
            LogUtils.info(Constants.BUNDLE_NAME, "setWritable is successful");
        }
    }

    private void finallyMethod(ZipFileInZipEntry found) {
        try {
            if (found != null && found.zipFile != null) {
                found.zipFile.close();
            }
        } catch (IOException ignored) {
            LogUtils.info(Constants.BUNDLE_NAME, Constants.TAG_VAL);
        }
    }

    /**
     * Copies all data from an {@link InputStream} to an {@link OutputStream}.
     *
     * @param in The stream to read from.
     * @param out The stream to write to.
     * @return The actual number of bytes copied
     * @throws IOException when a stream operation fails.
     */
    private long copy(InputStream in, OutputStream out) throws IOException {
        long copied = Constants.COPY_NUM_CONST;
        byte[] buf = new byte[COPY_BUFFER_SIZE];
        while (true) {
            int read = in.read(buf);
            if (read == Constants.READ_NUM_CONST) {
                break;
            }
            out.write(buf, 0, read);
            copied += read;
        }
        out.flush();
        return copied;
    }

    /**
     * Closes a {@link Closeable} silently (without throwing or handling any exceptions)
     * @param closeable {@link Closeable} to close
     */
    private void closeSilently(final Closeable closeable) {
        try {
            if (closeable != null) {
                closeable.close();
            }
        } catch (IOException ignored) {
            LogUtils.info(Constants.BUNDLE_NAME, "ignore");
        }
    }
}
